package pl.edu.agh.gp.fitness;

import org.apache.log4j.Logger;
import org.jgap.gp.GPFitnessFunction;
import org.jgap.gp.IGPProgram;
import org.jgap.gp.terminal.Variable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import pl.edu.agh.model.core.Price;
import pl.edu.agh.strategies.BuyAndHold;
import pl.edu.agh.types.TransactionType;
import pl.edu.agh.types.VariableType;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static pl.edu.agh.types.TransactionType.BUY;
import static pl.edu.agh.types.TransactionType.SELL;
import static pl.edu.agh.types.VariableType.*;

@Component
public class OverBHFitnessFunction extends GPFitnessFunction {
    private final double FINANCIAL_MIN = -10000000;   // 10 mln
    private Logger log = Logger.getLogger(OverBHFitnessFunction.class);

    private @Autowired BuyAndHold buyAndHold;
    private @Value("${lastDayEval}") boolean lastDayEval;
    private @Value("${maxArity}") int maxArity;

    private Map<VariableType, Variable> variables;
    private List<Price> buyAndHoldPrices;
    private Object[] noArgs = new Object[0];
    private IGPProgram bestProgram;

    private double initialMoney;
    private double bestFitness;
    private double shares;
    private double money;
    private int index;
    private double surplus;

    public List<Price> getIncome(final IGPProgram ind){
        List<Price> pricesForPlay = copyPrices();
        playOnWith(pricesForPlay, ind);
        return pricesForPlay;
    }

    protected double evaluate(final IGPProgram ind) {
        return playOnWith(copyPrices(), ind);
    }

    private double playOnWith(List<Price> pricesForPlay, final IGPProgram ind){
        log.debug("Playing on " + pricesForPlay.size() + " prices with strategy: " + ind.toStringNorm(0));

        money = initialMoney;
        shares = 0.0;
        surplus = 0.0;
        index = 0;

        for(Price price: pricesForPlay){
            setPricesListVar(pricesForPlay, price);
            setPriceVars(price);

            // create tree
            TransactionType transactionType = getTransactionType(ind.execute_boolean(0,noArgs));
            double transactionShares = getPositive(ind.execute_double(1,noArgs));
            double transactionMoney = transactionShares * price.getClose();

            log.trace("Extracted order. Buy? " + transactionType + ", sharesSize:" + transactionShares + ", transactionMoney: " + transactionMoney);

            makeTransaction(transactionType, transactionMoney, transactionShares, price.getClose());
            compareResults(price);
        }

        log.trace("Fitness: " + surplus);
        surplus = setBestSolution(ind, surplus);

        return surplus;
    }

    private void setPricesListVar(List<Price> pricesForPlay, Price price) {
        int currentPriceIdx = pricesForPlay.indexOf(price);
        int minIndex = (int) getPositive(currentPriceIdx - maxArity);
        variables.get(PRICES).set(pricesForPlay.subList(minIndex, currentPriceIdx));
    }

    private void setPriceVars(Price price) {
        variables.get(OPEN).set(price.getOpen());
        variables.get(HIGH).set(price.getHigh());
        variables.get(LOW).set(price.getLow());
        variables.get(CLOSE).set(price.getClose());
        variables.get(VOLUME).set(price.getVolume().doubleValue());
    }

    private TransactionType getTransactionType(boolean val){
        return val ? BUY : SELL;
    }

    private double getPositive(double val){
        return val < 0.0 ? 0.0 : val;
    }

    private void makeTransaction(TransactionType transactionType, double transactionMoney, double transactionShares, double closePrice){
        switch (transactionType){
            case BUY:
                if(money >= transactionMoney){
                    money -= transactionMoney;
                    shares += transactionShares;
                }else{
                    shares +=  money / closePrice;
                    money = 0.0;
                }
                break;
            case SELL:
                if(shares >= transactionShares){
                    shares -= transactionShares;
                    money += transactionMoney;
                }else{
                    money += shares * closePrice;
                    shares = 0.0;
                }
                break;
        }
    }

    private void compareResults(Price price) {
        double bhIncome = buyAndHoldPrices.get(index++).getIncome();
        price.setIncome(money + shares * price.getClose());
        double gpIncome = price.getIncome();

        log.trace("Current money: " + money + ", shares: " + shares + ", surplus: " + surplus);
        log.trace("[UI] Incomes: (BH: " + bhIncome + ", GP: " + gpIncome + ")");

        if(lastDayEval){
            surplus = gpIncome - bhIncome;
        }else{
            surplus += gpIncome - bhIncome;
        }
    }

    private double setBestSolution(IGPProgram ind, double surplus) {
        if(surplus > bestFitness){
            log.trace("Found better solution");

            bestFitness = surplus;
            bestProgram = ind;
        }
        if(surplus < 0.0){
            log.trace("Surplus lower than 0.0");
            return 0.0;
        }
        return surplus;
    }

    private List<Price> copyPrices() {
        List<Price> prices = new LinkedList<Price>();
        for(Price price: buyAndHoldPrices){
            prices.add(new Price(price.getDescription(), price.getDate(), price.getOpen(), price.getHigh(), price.getLow(), price.getClose(), price.getIncome(), price.getVolume()));
        }
        return prices;
    }

    public IGPProgram getBestProgram() {
        return bestProgram;
    }

    public double getBestFitness() {
        return bestFitness;
    }

    public void setInitialMoney(double initialMoney) {
        this.initialMoney = initialMoney;
        // can't go further, at this point we lost all money
        bestFitness = FINANCIAL_MIN;
    }

    public void setPrices(List<Price> prices) {
        this.buyAndHoldPrices = buyAndHold.play(initialMoney, prices);
    }

    public void setVariables(Map<VariableType, Variable> variables) {
        this.variables = variables;
    }
}